/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/models/ink_surface_model.h>

using namespace journal::model;

const baja::uint32 ink_surface_model::_inkSurfaceCreatePass = 0;
const baja::uint32 ink_surface_model::_inkStrokesCreatePass = 2;
const baja::uint32 ink_surface_model::_inkStrokesReadyPass = 2;

ink_surface_model::ink_surface_model(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent,
    std::shared_ptr<journal::model::db::iink_surface_table> inkSurfaceTable,
    const b::uint32 inkSurfaceId
        ) : _initialized(false),
            _inDB(false),
            _disposed(false),
            _parent(parent), 
            _inkSurfaceTable(inkSurfaceTable),
            _strokes(), 
            _strokeCollection(_strokes),
            _inkStrokesIdsReady(false),
            _allInkStrokesBlobSize(0)
{
    // generic_model
    setGenericIdentity(inkSurfaceId);
    setGenericTypeToken(typeToken);
    setGenericParent(std::dynamic_pointer_cast<bm::igeneric_model>(parent));
    // tracked_object
    setTrackingService(constants::undoRedoService());

    constants::memoryTrackingService()->logCreate(genericTypeToken(), genericIdentity(), sizeof(ink_surface_model));
}

ink_surface_model::~ink_surface_model()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"XX ink_surface_model::~ink_surface_model() : genericIdentity() = %i") %
        genericIdentity()
        );

    constants::memoryTrackingService()->logDispose(genericTypeToken(), genericIdentity(), sizeof(ink_surface_model));
}

std::shared_ptr<ink_surface_model> ink_surface_model::create(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent,
    std::shared_ptr<journal::model::db::iink_surface_table> inkSurfaceTable,
    const b::uint32 inkSurfaceId)
{
    std::shared_ptr<ink_surface_model> returnValue = std::make_shared<ink_surface_model>(typeToken, parent, inkSurfaceTable, inkSurfaceId);
    returnValue->_inkStrokeFactory = journal::model::createInkStrokeModelFactory();
    return returnValue;
}

void ink_surface_model::initialize()
{
    _initialized = true;

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> ink_surface_model::initialize() : id =  %i : firing onInkSurfaceModelReady()") %
        genericIdentity()
        );

    _observerSource.raise(
        [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
        {
            observer->onInkSurfaceModelReady(shared_from_this());
        });
}

b::Boolean ink_surface_model::isAccessible()
{
    return _initialized && !_disposed;
}

std::shared_ptr<ipage_model> ink_surface_model::parent()
{
    return _parent.lock();
}

void ink_surface_model::saveInkStroke(const std::shared_ptr<iink_stroke_model>& stroke)
{
    b::stopwatch ws;

    BAJA_LOCK(_mutex);

    std::shared_ptr<iinternal_model_blob> strokeBlob = std::dynamic_pointer_cast<iinternal_model_blob>(stroke);

    b::uint32 offset = 0;

    s::binary_blob blob(strokeBlob->blobSize());

    strokeBlob->packToBlob(blob, offset);

    _allInkStrokesBlobSize += offset;

    _inkSurfaceTable->appendInkStroke(genericIdentity(), _parent.lock()->storageId(), _allInkStrokesBlobSize, blob);

    constants::performanceService()->collect(performance_event::modelSave, ws.getElapsedMilliseconds()); 
}

void ink_surface_model::saveInkStrokeAsync(const std::shared_ptr<iink_stroke_model>& stroke)
{
    std::shared_ptr<ink_surface_model> p_this = std::static_pointer_cast<ink_surface_model>(shared_from_this());
    concurrency::task<void> t([p_this, stroke]()
    {
        p_this->saveInkStroke(stroke);
    });
}

void ink_surface_model::observeInkStroke(
    const std::shared_ptr<iink_stroke_model>& inkStroke)
{
    std::shared_ptr<bm::igeneric_model> genericModel = std::dynamic_pointer_cast<bm::igeneric_model>(inkStroke);
    genericModel->addGenericObserver( std::dynamic_pointer_cast<bm::igeneric_model_observer>(shared_from_this()) );
}

void ink_surface_model::unobserveInkStroke(
    const std::shared_ptr<iink_stroke_model>& inkStroke)
{
    std::shared_ptr<bm::igeneric_model> genericModel = std::dynamic_pointer_cast<bm::igeneric_model>(inkStroke);
    genericModel->removeGenericObserver( std::dynamic_pointer_cast<bm::igeneric_model_observer>(shared_from_this()) );
}

void ink_surface_model::attachInkStroke(const std::shared_ptr<iink_stroke_model>& stroke) 
{
    {
        BAJA_LOCK(_mutex);
        if (!isAccessible())
        {
            return;
        }

        _strokes.push_back(stroke);
        observeInkStroke(stroke);

        // we save here because strokes cannot save themselves.
        saveInkStrokeAsync(stroke);
    }

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> ink_surface_model::attachInkStroke() : id = %i : firing onStrokeModelAttached(stroke = %i)") %
        genericIdentity() %
        stroke->storageId()
        );

    fireChangedEvent();

    // raise stroke added event
    _observerSource.raise(
        [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
        {
            observer->onStrokeModelAttached(stroke);
        });
}

std::shared_ptr<iink_stroke_collection> ink_surface_model::inkStrokes() 
{
    if (!isAccessible())
    {
        return nullptr;
    }
    return b::make_stack_shared_ptr<iink_stroke_collection>(&_strokeCollection);
}

std::shared_ptr<iink_stroke_model> ink_surface_model::createInkStroke()
{
    if (!isAccessible())
    {
        return nullptr;
    }

    return _inkStrokeFactory->createInkStroke(shared_from_this());
}

void ink_surface_model::removeInkStroke(const b::uint32& id)
{
    std::shared_ptr<iink_stroke_model> strokeToRemove;
    {
        BAJA_LOCK(_mutex);
        if (!isAccessible())
        {
            return;
        }

        std::vector<std::shared_ptr<iink_stroke_model>> tempStrokes;

        b::uint32 strokeToRemoveSize = 0;

        for (auto &stroke : _strokes)
        {
            if ( stroke->storageId() == id)
            {
                strokeToRemove = stroke;
                std::shared_ptr<iinternal_model_blob> strokeBlob = std::dynamic_pointer_cast<iinternal_model_blob>(stroke);
                strokeToRemoveSize = strokeBlob->blobSize();
            }
            else
            {
                tempStrokes.push_back(stroke);
            }
        }

        if (strokeToRemove != nullptr)
        {
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
                boost::wformat(L">> <<>> ink_surface_model::removeInkStroke() : ink surface id = %i, stroke id = %i") %
                genericIdentity() %
                strokeToRemove->storageId()
                );

            unobserveInkStroke(strokeToRemove);

            // create a new blob...
            _allInkStrokesBlobSize -= strokeToRemoveSize;
            s::binary_blob strokesBlob(_allInkStrokesBlobSize);

            // rebuild the vector and populate the blob...
            b::uint32 offset = 0;
            _strokes.clear();
            std::vector<std::shared_ptr<iink_stroke_model>>::iterator tempStrokeIt;
            for (tempStrokeIt = tempStrokes.begin(); tempStrokeIt < tempStrokes.end(); ++tempStrokeIt)
            {
                _strokes.push_back(*tempStrokeIt);

                std::shared_ptr<iinternal_model_blob> strokeBlob = std::dynamic_pointer_cast<iinternal_model_blob>(*tempStrokeIt);
                strokeBlob->packToBlob(strokesBlob, offset);
            }

            // save new blob to disk...
            _inkSurfaceTable->replaceInkStrokes(genericIdentity(), _parent.lock()->storageId(), strokesBlob);
        }
    }

    if (strokeToRemove != nullptr)
    {
        // announce the change...
        fireChangedEvent();

        // announce the removal...
        _observerSource.raise(
            [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
            {
                observer->onStrokeModelRemoved(strokeToRemove);
            });
    }
}

b::uint32 ink_surface_model::storageId()
{
    return genericIdentity();
}

void ink_surface_model::removeFromDB(
    const std::shared_ptr<journal::model::db::iink_surface_table>& inkSurfaceTable,
    const b::uint32& pageId)

{
    inkSurfaceTable->dispose(pageId);
}

void ink_surface_model::createInDB()
{
    b::stopwatch ws;

    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }
    if (_inDB == true)
    {
        return;
    }

    DEBUG_MODEL_DUMP(this, false, L"<< ink_surface_model::saveSelf() : saving");

    _inkSurfaceTable->insert(
        genericIdentity(),
        _parent.lock()->storageId());

    _inDB = true;

    constants::performanceService()->collect(performance_event::modelSave, ws.getElapsedMilliseconds()); 
}

void ink_surface_model::loadSelf()
{
    BAJA_LOCK(_mutex);

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">>                      self : genericIdentity() = %i") %
        genericIdentity()
        );

    // No need to load 'self' since the only attribute is pageId.
    // And since this object has been created, it means that it already
    // belongs to a page, and the parent is set to point to that page.
    // Note that foreign keys are read before the object creation,
    // and fed to the object at its creation time.

    // And attributes go in here...

    _initialized = true;
    _inDB = true;
    DEBUG_MODEL_DUMP(this, false, L">> ink_surface_model loaded ");
}

void ink_surface_model::loadInkStrokes(baja::uint32 pass, bool& allDone)
{
    if (pass == _inkStrokesCreatePass && _strokes.size() == 0)
    {
        BAJA_LOCK(_mutex);

        s::binary_blob allStrokesBlob;

        b::stopwatch sw;

        _inkSurfaceTable->loadInkStrokes(genericIdentity(), _parent.lock()->storageId(), allStrokesBlob);

        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
            boost::wformat(L">> <<>> ink_surface_model::loadInkStrokes() : id = %i : pass = %i : loading strokes blob took %f ms : blob size = %i") %
            genericIdentity() %
            pass %
            sw.getElapsedMilliseconds() %
            allStrokesBlob._size
            );

        b::uint32 offset = 0;

        while (offset < allStrokesBlob._size)
        {
            // create a new stroke...
            std::shared_ptr<iink_stroke_model> inkStroke = _inkStrokeFactory->recreateInkStroke(shared_from_this(), allStrokesBlob, offset);
            _strokes.push_back(inkStroke);
        }

        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
            boost::wformat(L">> <<>> ink_surface_model::loadInkStrokes() : id = %i : pass = %i : created %i strokes") %
            genericIdentity() %
            pass %
            static_cast<b::uint32>(_strokes.size())
            );

        _allInkStrokesBlobSize = allStrokesBlob._size;
    }

    if (pass == _inkStrokesReadyPass)
    {
        concurrency::concurrent_vector<std::shared_ptr<iink_stroke_model>> strokes = _strokes;
        for (auto &stroke : strokes)
        {
            _observerSource.raise(
                [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
                {
                    observer->onStrokeModelReady(stroke);
                });
        }
        allDone = true;
    }
}

void ink_surface_model::load(baja::uint32 pass, bool& done)
{
    if (_disposed == true)
    {
        return;
    }
    b::stopwatch ws;

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> ink_surface_model loading : id = %i : pass = %i") %
        genericIdentity() %
        pass
        );

    if (_initialized == false)
    {
        loadSelf();
    }

    if (pass == _inkSurfaceCreatePass)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
            boost::wformat(L">> <<>> ink_surface_model::load() : id = %i : pass = %i : firing onInkSurfaceModelReady()") %
            genericIdentity() %
            pass
            );

        _observerSource.raise(
            [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
            {
                observer->onInkSurfaceModelReady(shared_from_this());
            });
    }

    bool allStrokesLoadDone = false;
    if (pass >= _inkStrokesCreatePass)
    {
        loadInkStrokes(pass, allStrokesLoadDone);
    }

    if (allStrokesLoadDone == true)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
            boost::wformat(L">> <<>> ink_surface_model::load() : id = %i : pass = %i : firing onInkSurfaceChildrenReady()") %
            genericIdentity() %
            pass
            );

        _observerSource.raise(
            [&](const std::shared_ptr<iink_surface_model_observer>& observer, void* context)
            {
                observer->onInkSurfaceChildrenReady(shared_from_this());
            });
    }

    done = allStrokesLoadDone;

    constants::performanceService()->collect(performance_event::modelLoad, ws.getElapsedMilliseconds()); 
}

void ink_surface_model::uncreateInkStrokes()
{
    _strokes.clear();
}

void ink_surface_model::unload()
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }

    uncreateInkStrokes();
}

void ink_surface_model::dispose()
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }

    // dispose children.
    // there are no children...

    // dispose self
    if (_inDB == false)
    {
        return;
    }

    removeFromDB(_inkSurfaceTable, _parent.lock()->storageId());
    _inDB = false;
    _initialized = false;
    _disposed = true;
}

b::uint32 ink_surface_model::sizeInMemory()
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return 0;
    }

    b::uint32 size = 0;
    b::uint32 inkSurfaceId = genericIdentity();

    size = sizeof(inkSurfaceId) + sizeof(b::uint32);

    concurrency::concurrent_vector<std::shared_ptr<iink_stroke_model>>::iterator strokeIt;
    for (strokeIt = _strokes.begin(); strokeIt < _strokes.end(); ++strokeIt)
    {
        std::shared_ptr<iinternal_ink_stroke_model> stroke = std::dynamic_pointer_cast<iinternal_ink_stroke_model>(*strokeIt);
        size += stroke->sizeInMemory();
    }
    return size;
}

#ifdef DEBUG_ENABLE_MODELS
void ink_surface_model::dump(bool includeChildren, std::wstring prefix)
{
    BAJA_LOCK(_mutex);

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"%s : genericIdentity() = %i,\t_pageId = %i, size in memory = %i,\t_initialized = %s,\t_inDB = %s") %
        prefix.c_str() %
        genericIdentity() %
        _parent.lock()->storageId() %
        this->sizeInMemory() %
        (_initialized ? L"true" : L"false") %
        (_inDB ? L"true" : L"false")
        );

    if (includeChildren == false)
    {
        return;
    }

    concurrency::concurrent_vector<std::shared_ptr<iink_stroke_model>>::iterator strokeIt;
    for (strokeIt = _strokes.begin(); strokeIt < _strokes.end(); ++strokeIt)
    {
        DEBUG_MODEL_DUMP((*strokeIt), includeChildren, prefix);
    }

    for (auto& stroke : _strokes)
    {
        baja::app::debugConsoleTrace(
            (boost::wformat(L"    DEFINE_STROKE(Stroke%i)\n") %
            stroke->storageId()
            ).str().c_str(),
            TRACE_TAG_RECORDED,
            true);
    }

}
#endif

baja::int32 ink_surface_model::addObserver(const std::shared_ptr<iink_surface_model_observer>& observer)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> ink_surface_model::addObserver() : id = %i : an observer (%i) has been added.") %
        genericIdentity() %
        reinterpret_cast<b::uint32>(observer.get())
        );

    return _observerSource += observer;
}

void ink_surface_model::removeObserver(const std::shared_ptr<iink_surface_model_observer>& observer)
{
    _observerSource -= observer;
}

void ink_surface_model::removeObserver(const b::int32 cookie)
{
    _observerSource -= cookie;
}

void ink_surface_model::onChildChanged(
    const b::uint32& typeToken,
    const b::uint32& identity
    )
{
}

std::shared_ptr<iink_surface_model> journal::model::createInkSurface(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent,
    std::shared_ptr<journal::model::db::iink_surface_table> inkSurfaceTable,
    const b::uint32 inkSurfaceId)
{
    std::shared_ptr<ink_surface_model> returnValue = ink_surface_model::create(typeToken, parent, inkSurfaceTable, inkSurfaceId);
    return std::dynamic_pointer_cast<iink_surface_model>(returnValue);
}

void journal::model::removeInkSurfaces(
    const std::shared_ptr<journal::model::db::iink_surface_table>& inkSurfaceTable,
    const b::uint32& pageId)
{
    ink_surface_model::removeFromDB(inkSurfaceTable, pageId);
}
